一份关于 TypeScript 'infer' 关键字的综合指南,解释了如何将其与条件类型一起使用,以进行强大的类型提取和操作,包括高级用例。
掌握 TypeScript Infer:用于高级类型操作的条件类型提取
TypeScript 的类型系统非常强大,允许开发人员创建健壮且可维护的应用程序。实现这种能力的关键特性之一是与条件类型结合使用的 infer
关键字。这种组合提供了一种从复杂类型结构中提取特定类型的机制。这篇博文深入探讨了 infer
关键字,解释了它的功能并展示了高级用例。我们将探索适用于各种软件开发场景的实际示例,从 API 交互到复杂的数据结构操作。
什么是条件类型?
在我们深入研究 infer
之前,让我们快速回顾一下条件类型。TypeScript 中的条件类型允许您根据条件定义类型,类似于 JavaScript 中的三元运算符。基本语法是:
T extends U ? X : Y
这可以理解为:“如果类型 T
可分配给类型 U
,则该类型为 X
;否则,该类型为 Y
。”
例子:
type IsString<T> = T extends string ? true : false;
type StringResult = IsString<string>; // type StringResult = true
type NumberResult = IsString<number>; // type NumberResult = false
介绍 infer
关键字
infer
关键字用于条件类型的 extends
子句中,以声明一个可以从正在检查的类型中推断出的类型变量。本质上,它允许您“捕获”类型的一部分以供以后使用。
基本语法:
type MyType<T> = T extends (infer U) ? U : never;
在此示例中,如果 T
可分配给某种类型,TypeScript 将尝试推断 U
的类型。如果推断成功,则该类型将为 U
;否则,它将为 never
。
infer
的简单示例
1. 推断函数的返回类型
一个常见的用例是推断函数的返回类型:
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
function add(a: number, b: number): number {
return a + b;
}
type AddReturnType = ReturnType<typeof add>; // type AddReturnType = number
function greet(name: string): string {
return `Hello, ${name}!`;
}
type GreetReturnType = ReturnType<typeof greet>; // type GreetReturnType = string
在此示例中,ReturnType<T>
采用函数类型 T
作为输入。它检查 T
是否可分配给接受任何参数并返回值的函数。如果是,它会将返回类型推断为 R
并返回它。否则,它返回 any
。
2. 推断数组元素类型
另一个有用的场景是从数组中提取元素类型:
type ArrayElementType<T> = T extends (infer U)[] ? U : never;
type NumberArrayType = ArrayElementType<number[]>; // type NumberArrayType = number
type StringArrayType = ArrayElementType<string[]>; // type StringArrayType = string
type MixedArrayType = ArrayElementType<(string | number)[]>; // type MixedArrayType = string | number
type NotAnArrayType = ArrayElementType<number>; // type NotAnArrayType = never
在这里,ArrayElementType<T>
检查 T
是否为数组类型。如果是,它会将元素类型推断为 U
并返回它。如果不是,它返回 never
。
infer
的高级用例
1. 推断构造函数的参数
您可以使用 infer
来提取构造函数的参数类型:
type ConstructorParameters<T extends new (...args: any) => any> = T extends new (...args: infer P) => any ? P : never;
class Person {
constructor(public name: string, public age: number) {}
}
type PersonConstructorParams = ConstructorParameters<typeof Person>; // type PersonConstructorParams = [string, number]
class Point {
constructor(public x: number, public y: number) {}
}
type PointConstructorParams = ConstructorParameters<typeof Point>; // type PointConstructorParams = [number, number]
在这种情况下,ConstructorParameters<T>
采用构造函数类型 T
。它推断构造函数参数的类型为 P
并将它们作为元组返回。
2. 从对象类型中提取属性
infer
也可以用于使用映射类型和条件类型从对象类型中提取特定属性:
type PickByType<T, K extends keyof T, U> = {
[P in K as T[P] extends U ? P : never]: T[P];
};
interface User {
id: number;
name: string;
age: number;
email: string;
isActive: boolean;
}
type StringProperties = PickByType<User, keyof User, string>; // type StringProperties = { name: string; email: string; }
type NumberProperties = PickByType<User, keyof User, number>; // type NumberProperties = { id: number; age: number; }
//An interface representing geographic coordinates.
interface GeoCoordinates {
latitude: number;
longitude: number;
altitude: number;
country: string;
city: string;
timezone: string;
}
type NumberCoordinateProperties = PickByType<GeoCoordinates, keyof GeoCoordinates, number>; // type NumberCoordinateProperties = { latitude: number; longitude: number; altitude: number; }
在这里,PickByType<T, K, U>
创建一个新类型,该类型仅包含 T
的属性(键在 K
中),其值可分配给类型 U
。映射类型迭代 T
的键,条件类型过滤掉不匹配指定类型的键。
3. 使用 Promise
您可以推断 Promise
的解析类型:
type Awaited<T> = T extends Promise<infer U> ? U : T;
async function fetchData(): Promise<string> {
return 'Data from API';
}
type FetchDataType = Awaited<ReturnType<typeof fetchData>>; // type FetchDataType = string
async function fetchNumbers(): Promise<number[]> {
return [1, 2, 3];
}
type FetchedNumbersType = Awaited<ReturnType<typeof fetchNumbers>>; //type FetchedNumbersType = number[]
Awaited<T>
类型接受一个类型 T
,该类型应为 Promise。然后,该类型推断 Promise 的解析类型 U
,并返回它。如果 T
不是 promise,则返回 T。这是 TypeScript 较新版本中的内置实用工具类型。
4. 提取 Promise 数组的类型
结合 Awaited
和数组类型推断,您可以推断由 Promise 数组解析的类型。这在处理 Promise.all
时特别有用。
type PromiseArrayReturnType<T extends Promise<any>[]> = {
[K in keyof T]: Awaited<T[K]>;
};
async function getUSDRate(): Promise<number> {
return 0.0069;
}
async function getEURRate(): Promise<number> {
return 0.0064;
}
const rates = [getUSDRate(), getEURRate()];
type RatesType = PromiseArrayReturnType<typeof rates>;
// type RatesType = [number, number]
此示例首先定义两个异步函数 getUSDRate
和 getEURRate
,它们模拟获取汇率。然后,PromiseArrayReturnType
实用工具类型从数组中的每个 Promise
中提取解析的类型,从而生成一个元组类型,其中每个元素是相应 Promise 的等待类型。
不同领域的实际示例
1. 电子商务应用
考虑一个电子商务应用程序,您从中获取产品详细信息API。您可以使用 infer
提取产品数据的类型:
interface Product {
id: number;
name: string;
price: number;
description: string;
imageUrl: string;
category: string;
rating: number;
countryOfOrigin: string;
}
async function fetchProduct(productId: number): Promise<Product> {
// Simulate API call
return new Promise((resolve) => {
setTimeout(() => {
resolve({
id: productId,
name: 'Example Product',
price: 29.99,
description: 'A sample product',
imageUrl: 'https://example.com/image.jpg',
category: 'Electronics',
rating: 4.5,
countryOfOrigin: 'Canada'
});
}, 500);
});
}
type ProductType = Awaited<ReturnType<typeof fetchProduct>>; // type ProductType = Product
function displayProductDetails(product: ProductType) {
console.log(`Product Name: ${product.name}`);
console.log(`Price: ${product.price} ${product.countryOfOrigin === 'Canada' ? 'CAD' : (product.countryOfOrigin === 'USA' ? 'USD' : 'EUR')}`);
}
fetchProduct(123).then(displayProductDetails);
在此示例中,我们定义了一个 Product
接口和一个 fetchProduct
函数,该函数从 API 获取产品详细信息。我们使用 Awaited
和 ReturnType
从 fetchProduct
函数的返回类型中提取 Product
类型,从而允许我们对 displayProductDetails
函数进行类型检查。
2. 国际化 (i18n)
假设您有一个翻译函数,该函数根据语言环境返回不同的字符串。您可以使用 infer
提取此函数的返回类型以确保类型安全:
interface Translations {
greeting: string;
farewell: string;
welcomeMessage: (name: string) => string;
}
const enTranslations: Translations = {
greeting: 'Hello',
farewell: 'Goodbye',
welcomeMessage: (name: string) => `Welcome, ${name}!`,
};
const frTranslations: Translations = {
greeting: 'Bonjour',
farewell: 'Au revoir',
welcomeMessage: (name: string) => `Bienvenue, ${name}!`,
};
function getTranslation(locale: 'en' | 'fr'): Translations {
return locale === 'en' ? enTranslations : frTranslations;
}
type TranslationType = ReturnType<typeof getTranslation>;
function greetUser(locale: 'en' | 'fr', name: string) {
const translations = getTranslation(locale);
console.log(translations.welcomeMessage(name));
}
greetUser('fr', 'Jean'); // Output: Bienvenue, Jean!
在这里,TranslationType
被推断为 Translations
接口,确保 greetUser
函数具有正确的类型信息以访问翻译后的字符串。
3. API 响应处理
在使用 API 时,响应结构可能很复杂。 infer
可以帮助从嵌套的 API 响应中提取特定的数据类型:
interface ApiResponse<T> {
status: number;
data: T;
message?: string;
}
interface UserData {
id: number;
username: string;
email: string;
profile: {
firstName: string;
lastName: string;
country: string;
language: string;
}
}
async function fetchUser(userId: number): Promise<ApiResponse<UserData>> {
// Simulate API call
return new Promise((resolve) => {
setTimeout(() => {
resolve({
status: 200,
data: {
id: userId,
username: 'johndoe',
email: 'john.doe@example.com',
profile: {
firstName: 'John',
lastName: 'Doe',
country: 'USA',
language: 'en'
}
}
});
}, 500);
});
}
type UserApiResponse = Awaited<ReturnType<typeof fetchUser>>;
type UserProfileType = UserApiResponse['data']['profile'];
function displayUserProfile(profile: UserProfileType) {
console.log(`Name: ${profile.firstName} ${profile.lastName}`);
console.log(`Country: ${profile.country}`);
}
fetchUser(123).then((response) => {
if (response.status === 200) {
displayUserProfile(response.data.profile);
}
});
在此示例中,我们定义了一个 ApiResponse
接口和一个 UserData
接口。我们使用 infer
和类型索引从 API 响应中提取 UserProfileType
,确保 displayUserProfile
函数接收正确的类型。
使用 infer
的最佳实践
- 保持简单: 仅在必要时使用
infer
。过度使用它会使您的代码更难阅读和理解。 - 记录您的类型: 添加注释以解释您的条件类型和
infer
语句的作用。 - 测试您的类型: 使用 TypeScript 的类型检查来确保您的类型按预期运行。
- 考虑性能: 复杂的条件类型有时会影响编译时间。请注意您的类型的复杂性。
- 使用实用工具类型: TypeScript 提供了几种内置实用工具类型(例如,
ReturnType
、Awaited
),可以简化您的代码并减少对自定义infer
语句的需求。
常见陷阱
- 不正确的推断: 有时,TypeScript 可能会推断出不是您期望的类型。仔细检查您的类型定义和条件。
- 循环依赖: 使用
infer
定义递归类型时要小心,因为它们会导致循环依赖和编译错误。 - 过于复杂的类型: 避免创建过于复杂的条件类型,这些类型难以理解和维护。将它们分解为更小、更易于管理的类型。
infer
的替代方案
虽然 infer
是一个强大的工具,但在某些情况下,替代方法可能更合适:
- 类型断言: 在某些情况下,您可以使用类型断言来显式指定值的类型,而不是推断它。但是,请谨慎使用类型断言,因为它们会绕过类型检查。
- 类型守卫: 类型守卫可用于根据运行时检查缩小值的类型。当您需要根据运行时条件处理不同类型时,这非常有用。
- 实用工具类型: TypeScript 提供了丰富的实用工具类型集,可以处理许多常见的类型操作任务,而无需自定义
infer
语句。
结论
TypeScript 中的 infer
关键字与条件类型结合使用时,可以解锁高级类型操作功能。它允许您从复杂类型结构中提取特定类型,使您能够编写更健壮、可维护且类型安全的代码。从推断函数返回类型到从对象类型中提取属性,可能性是巨大的。通过理解本指南中概述的原则和最佳实践,您可以充分利用 infer
的潜力并提高您的 TypeScript 技能。请记住记录您的类型,彻底测试它们,并在适当的时候考虑替代方法。掌握 infer
使您能够编写真正富有表现力和强大的 TypeScript 代码,最终带来更好的软件。